﻿/* Copyright (C) Siemens AG, 2012. All Rights Reserved.
 * Author: Roland Eckl, Corporate Technology
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.Windows.Input;
using RVM.Helpers;
using RVM.Infrastructure;
using RVM.Infrastructure.Communication;


namespace RVM
{
    /// <summary>
    /// A RemotableViewModel is a ViewModel that can (not necessarily must) be exported to cross process boundaries.
    /// MarshalByRefObject is the base class to enable .NET Remoting. In case of WCF marshalling is always done by value anyhow.
    /// TODO: Move code shared among RVM and Proxy to separate base class. (e.g. CreateCollection is not relevant for proxy but still there at the moment)
    /// </summary>
    public class RemotableViewModel : INotifyPropertyChanged
    {
        #region Infrastructure

        #region Private and Protected Members

        /// <summary>
        /// Unique ID for the ViewModel.
        /// </summary>
        protected string _identifier = string.Empty;

        /// <summary>
        /// Flag if the ViewModel is currently exported.
        /// </summary>
        protected bool _isExported = false;

        /// <summary>
        /// List if remote agents, currently importing this ViewModel.
        /// </summary>
        protected readonly List<IViewModelAgent> _importingAgents = new List<IViewModelAgent>();

        /// <summary>
        /// Importer used to create this viewmodel. Used to create nested viewmodels!
        /// (On remote-/exporter-side always null)
        /// </summary>
        protected IViewModelImporter _importer = null;

        /// <summary>
        /// Registry used to add nested ViewModels into same registry. Reference to exporter thus
        /// not required. (On remote-/exporter-side always null)
        /// </summary>
        protected ViewModelRegistry _associatedRegistry = null;

        /// <summary>
        /// Proxy to the exporting/remote agent.
        /// </summary>
        protected IViewModelAgent _localProxy = null;

        /// <summary>
        /// Callback Channel, receives events from the exporting/remote agent.
        /// </summary>
        protected IViewModelCallback _localCallbackChannel = null;

        /// <summary>
        /// Dictionary that holds all properties.
        /// Instead of private members all properties are kept in in key (=property name) value (=value) pairs.
        /// </summary>
        protected Dictionary<string, object> _properties = new Dictionary<string, object>();

        /// <summary>
        /// Private Member of the ViewModelLocation.
        /// </summary>
        protected ViewModelLocation _viewModelLocation = ViewModelLocation.Local;

        #endregion

        #region Public Members

        /// <summary>
        /// Read-Only property for the ViewModelLocation.
        /// The location information can only be set from within the RemotableViewModel
        /// when injecting the remote object.
        /// TODO: Is this not equal to _localProxy!=null==ViewModelLocation.Remote?
        /// </summary>
        public ViewModelLocation Location { get { return _viewModelLocation; } }

        /// <summary>
        /// Number of associated peers.
        /// A ViewModel representing just the proxy for an exporting source has always exactly 1 peer.
        /// An exporting source has as many peers as importers are currently available. An exporting
        /// source with no importer has 0 peers.
        /// </summary>
        public int PeerCount
        {
            get
            {
                if (_viewModelLocation == ViewModelLocation.Remote)
                    return 1; // This ViewModel can only have a single exporting source.

                return _importingAgents.Count;
            }
        }
        #endregion

        #region Public Methods

        /// <summary>
        /// Registers ViewModel at local registry, which is available for all Exporters.
        /// </summary>
        /// <param name="id">Identifier in registry</param>
        public void ExportViewModel(string id)
        {
            ExportViewModel(id, ViewModelRegistry.Shared);
        }

        /// <summary>
        /// Registers ViewModel at local registry, which is available for all Exporters.
        /// </summary>
        /// <param name="id">Identifier in registry</param>
        /// <param name="registry">Registry where to expose this viewmodel</param>
        public void ExportViewModel(string id, ViewModelRegistry registry)
        {
            // TODO: Better check if already part of the actual registry.
            if (!_isExported)
            {
                _isExported = true;
                _identifier = id;
                registry.Add(id, this);
                _associatedRegistry = registry;
            }            
        }

        /// <summary>
        /// Gets the value for a specific property.
        /// If the ViewModel is from the application's point of view a local one,
        /// the value is taken from the _properties dictionary. If it is a remote one,
        /// the proxy towards the remote ViewModel is called.
        /// </summary>
        /// <param name="propertyName">Property of interest</param>
        /// <param name="defaultValue">Value if there is no content (yet)</param>
        /// <returns>Current value of the property</returns>
        public virtual object GetValue(string propertyName, object defaultValue = null)
        {
            // If there is a remote object, get it's value
            if (_localProxy != null)
            {
                try
                {
                    if (IsNestedViewModel(propertyName) && _importer != null) // For local case the viewmodel can simply be taken from the dictionary.
                    {
                        // If ViewModel was already set, do not import again! (When new viewmodel is set, the value has to be nulled on property changed)
                        object cache = null;
                        _properties.TryGetValue(propertyName, out cache);
                        if (cache != null)
                        {
                            if(cache is ViewModelProxy) // traditional viewmodels have a proxy, return associated model instead.
                                return ((ViewModelProxy)cache).Associated;
                            return cache;
                        }

                        // Otherwise, import...
                        PropertyInfo info = this.GetType().GetProperty(propertyName);
                        object vm = _importer.GetViewModel(info.PropertyType, _identifier + "." + propertyName);
                        _properties[propertyName] = vm;
                        if (vm is ViewModelProxy) // traditional viewmodels have a proxy, return associated model instead.
                            return ((ViewModelProxy) vm).Associated;
                        return vm;
                    }

                    return _localProxy.GetValue(_identifier, propertyName, defaultValue);
                }
                catch(Exception ex)
                {
                    // Seems the proxy is corrupt,
                    RemoveRemoteViewModelAgent();
                }
            }

            object returnValue = null;
            _properties.TryGetValue(propertyName, out returnValue);
            if (returnValue == null)
                return defaultValue;

            // TODO: Dead code?
            if(returnValue is INotifyCollectionChanged)
            {
                var x = returnValue as IList;

                object[] m = new object[x.Count];
                for (int i = 0; i < x.Count; i++ )
                {
                    m[i] = x[i];
                }

                return m;
            }

            return returnValue;
        }

        /// <summary>
        /// Sets the value for a specific property.
        /// If the ViewModel is from the application's point of view a local one,
        /// the value is put into the _properties dictionary. If it is a remote one,
        /// the proxy towards the remote ViewModel is called.
        /// </summary>
        /// <param name="propertyName">Property that changed</param>
        /// <param name="value">New value</param>
        /// <param name="supressNotification">Fire PropertyChanged event for this set-operation?</param>
        public virtual void SetValue(string propertyName, object value, bool supressNotification = false)
        {
            // If there is an exporting/remote object, set it's value
            if (_localProxy != null)
            {
                //////
                // Throw exception by design, because it would be too tricky to clone viewmodel (which is not necessarily serializable) on remote side.
                if(IsNestedViewModel(propertyName))
                    throw new ActionNotSupportedException("Can't set ViewModels from Importer side.");
                //
                //////
                
                try
                {
                    _localProxy.SetValue(_identifier, propertyName, value, supressNotification);
                }
                catch(Exception ex)
                {
                    // Seems the proxy is corrupted.
                    RemoveRemoteViewModelAgent();
                }
            }
            else
            {
                _properties[propertyName] = value;

                if (IsNestedViewModel(propertyName))
                {
                    RemotableViewModel rvm = _properties[propertyName] as RemotableViewModel;
                    if (rvm != null) // Is explicitely a RemoteViewModel, not only implementing INotifyPropertyChanged.
                        rvm.ExportViewModel(_identifier + "." + propertyName, _associatedRegistry);
                    else // Implements at least INotifyPropertyChanged.
                        new ViewModelProxy(value as INotifyPropertyChanged).ExportViewModel(_identifier + "." + propertyName, _associatedRegistry);
                }

                if (!supressNotification)
                    FirePropertyChanged(propertyName);
            }
        }

        #region Command-specific

        /// <summary>
        /// Calls Execute on a command either locally or on an exported remote partner.
        /// </summary>
        /// <param name="commandName">Command's name</param>
        /// <param name="parameter">Parameter to hand over</param>
        public virtual void ExecuteCommand(string commandName, object parameter)
        {
            // If there is an exporting/remote object, set it's value
            if (_localProxy != null)
            {
                try
                {
                    _localProxy.ExecuteCommand(_identifier, commandName, parameter);
                }
                catch(Exception ex)
                {
                    // Seems the proxy is corrupt,
                    RemoveRemoteViewModelAgent();
                }
            }
            else
            {
                object command = null;
                _properties.TryGetValue(commandName, out command);
                if (command != null && command is ICommand)
                    ((ICommand)command).Execute(parameter);
                else
                {
                    PropertyInfo propertyInfo = GetType().GetProperty(commandName);
                    ICommand actualCommand = propertyInfo.GetValue(this, null) as ICommand;
                    if(actualCommand != null)
                    {
                        _properties[commandName] = actualCommand;
                        actualCommand.Execute(parameter);
                    }
                }
            }
        }

        /// <summary>
        /// Calls CanExecute on a command either locally or on an exported remote partner.
        /// </summary>
        /// <param name="commandName">Command's name</param>
        /// <param name="parameter">Parameter to hand over</param>
        /// <returns>TRUE, if condition is met and command can be executed</returns>
        public virtual bool CanExecuteCommand(string commandName, object parameter)
        {
            // If there is an exporting/remote object, set it's value
            if (_localProxy != null)
            {
                try
                {
                    return _localProxy.CanExecuteCommand(_identifier, commandName, parameter);
                }
                catch (Exception ex)
                {
                    // Seems the proxy is corrupt,
                    RemoveRemoteViewModelAgent();
                }
            } 
            
            object command = null;
            _properties.TryGetValue(commandName, out command);
            if (command != null && command is ICommand)
            {
                bool can = ((ICommand)command).CanExecute(parameter);
                return can;
            }

            PropertyInfo propertyInfo = GetType().GetProperty(commandName);
            ICommand actualCommand = propertyInfo.GetValue(this, null) as ICommand;
            if (actualCommand != null)
            {
                _properties[commandName] = actualCommand;
                return actualCommand.CanExecute(parameter);
            }

            return true;
        }

        /// <summary>
        /// Creates a new command based on some existing command within the RemotableViewModel and links both.
        /// </summary>
        /// <param name="commandName">Property name of the command</param>
        /// <param name="command">Command implementing ICommand</param>
        /// <returns>RemotableCommand</returns>
        public ICommand CreateCommand(string commandName, ICommand command)
        {
            // TODO: Why not add the command to the properties dictionary? If already created, simply return the existing instance!
            return new RemotableCommand(commandName, command, this);
        }

        /// <summary>
        /// Creates a new command within the RemotableViewModel and links both.
        /// </summary>
        /// <param name="commandName">Property name of the command</param>
        /// <param name="execute">Action to be called when command is triggered</param>
        /// <param name="predicate">Predicate to check if command can be triggered</param>
        /// <returns>RemotableCommand</returns>
        public ICommand CreateCommand(string commandName, Action<object> execute, Predicate<object> predicate)
        {
            // TODO: Why not add the command to the properties dictionary? If already created, simply return the existing instance!
            return new RemotableCommand(commandName, execute, predicate, this);
        }

        #endregion

        #region Collection-specific

        /// <summary>
        /// Creates or returns a collection of the given type.
        /// </summary>
        /// <typeparam name="T">Type of collection</typeparam>
        /// <param name="collectionName">Name of the collection</param>
        /// <returns></returns>
        public ObservableCollection<T> CreateCollection<T>(string collectionName)
        {
            object val = null;
            _properties.TryGetValue(collectionName, out val);
            if (val == null)
            {
                INotifyCollectionChanged collection = new ObservableCollection<T>();
                _properties[collectionName] = collection;
                collection.CollectionChanged += OnLocalCollectionChanged;
                val = collection;

                // TODO: Sync with remote model, if on importer-side!
                if (_localProxy != null)
                {
                    var content = _localProxy.GetCollection(_identifier, collectionName);
                    if (content != null)
                    {
                        foreach (object item in content)
                        {
                            ((IList)val).Add(item);
                        }
                    }
                }

            }
            return val as ObservableCollection<T>;
        }

        /// <summary>
        /// Creates or returns a generic collection.
        /// </summary>
        /// <param name="collectionName">Name of the collection</param>
        /// <returns></returns>
        public ObservableCollection<object> CreateCollection(string collectionName)
        {
            return CreateCollection<object>(collectionName);
        }

        #endregion

        #endregion

        #region Internal Methods

        #region Collection-specific

        /// <summary>
        /// Gets a generic collection by name.
        /// </summary>
        /// <param name="collectionName"></param>
        /// <returns></returns>
        internal virtual IList GetCollection(string collectionName)
        {
            object val = null;
            _properties.TryGetValue(collectionName, out val);
            return val as IList;
        }

        /// <summary>
        /// Gets the inner type of a generic collection by name.
        /// </summary>
        /// <param name="collectionName"></param>
        /// <returns></returns>
        internal Type GetCollectionInnerType(string collectionName)
        {
            object val = null;
            _properties.TryGetValue(collectionName, out val);
            if (val != null)
            {
                Type t = val.GetType();
                Type[] gt = t.GetGenericArguments();
                if (gt.Length > 0)
                    return gt[0];
            }
            return null;
        }

        /// <summary>
        /// Event handler, is raised whenever an exporting/remote ViewModel's collection sends a
        /// collection changed event locally.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        internal virtual void OnLocalCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            // Local subscribers should get the CollectionChanged event themselves, nothing to do!
            if (_importingAgents.Count <= 0)
                return;

            // Get all properties (there could be several properties pointing to actually the same collection)
            IEnumerable<string> affected = from pair in _properties where Equals(pair.Value, sender) select pair.Key;

            // For remote subscribers...
            foreach (string collection in affected)
            {
                // For remote subscribers
                for (int i = _importingAgents.Count - 1; i >= 0; i--)
                {
                    try
                    {
                        CollectionChangedEventArgs details = new CollectionChangedEventArgs();
                        details.Action = e.Action;
                        details.NewItems = e.NewItems; //(e.Action == NotifyCollectionChangedAction.Add) ? e.NewItems : e.OldItems;
                        details.NewIndex = e.NewStartingIndex;
                        details.OldItems = e.OldItems;
                        details.OldIndex = e.OldStartingIndex;
                        _importingAgents[i].FireCollectionChanged(_identifier, collection, details);
                    }
                    catch (Exception)
                    {
                        // Subscriber possible got lost...
                        RemoveViewModelAgent(_importingAgents[i]);
                    }
                }
            }
        }

        /// <summary>
        /// Event handler, is raised whenever the exporting/remote ViewModel sends a
        /// collection changed event through the callback channel.
        /// </summary>
        /// <param name="id"></param>
        /// <param name="property"></param>
        /// <param name="details"></param>
        internal void OnRemoteCollectionChanged(string id, string property, CollectionChangedEventArgs details)
        {
            // Only fire events that are targeting this actual viewmodel
            if (_identifier != null && _identifier.Equals(id))
            {
                //Type t = GetCollectionType(property);
                IList x = GetCollection(property);
                if (x == null)
                    return; // there is no collection yet, means I cannot add something TODO: Create collection of the generic inner type assmed by reflecting the viewmodels property.

                // Simply dispatch into UI thread.
                SmartDispatcher.BeginInvoke(delegate
                {
                    switch (details.Action)
                    {
                        case NotifyCollectionChangedAction.Add:
                            for (int i = 0; i < details.NewItems.Count; i++)
                            {
                                x.Insert(details.NewIndex + i, details.NewItems[i]);
                            }
                            break;
                        case NotifyCollectionChangedAction.Remove:
                            for (int i = 0; i < details.OldItems.Count; i++)
                            {
                                x.RemoveAt(details.OldIndex);
                            }
                            break;
                        case NotifyCollectionChangedAction.Replace:
                            for (int i = 0; i < details.OldItems.Count; i++)
                            {
                                x.RemoveAt(details.OldIndex);
                            }
                            for (int i = 0; i < details.NewItems.Count; i++)
                            {
                                x.Insert(details.NewIndex + i, details.NewItems[i]);
                            }
                            break;
                        case NotifyCollectionChangedAction.Move:
                            List<object> items = new List<object>(details.OldItems.Count);
                            for (int i = details.OldIndex; i < details.OldItems.Count; i++)
                            {
                                items.Add(x[i]);
                            }
                            for (int i = 0; i < details.OldItems.Count; i++)
                            {
                                x.RemoveAt(details.OldIndex);
                            }
                            for (int i = 0; i < items.Count; i++)
                            {
                                x.Insert(details.NewIndex + i, items[i]);
                            }
                            break;

                        case NotifyCollectionChangedAction.Reset:
                            x.Clear();
                            for (int i = 0; i < details.NewItems.Count; i++)
                            {
                                x.Add(details.NewItems[i]);
                            }
                            break;

                    }
                });
            }
        }

        #endregion

        /// <summary>
        /// Checks wether a property is a (Remotable)ViewModel or not.
        /// </summary>
        /// <param name="propertyName">Property's Name</param>
        /// <param name="checkTraditional">Checks also for INotifyPropertyChanged</param>
        /// <returns>TRUE, if assignable from RemotableViewModel or INotifyPropertyChanged</returns>
        internal protected bool IsNestedViewModel(string propertyName, bool checkTraditional = true)
        {
            PropertyInfo info = null;

            // RemotableViewModels have properties inside themselves, traditional ones are linked to their proxy.
            if(this.GetType() == typeof(ViewModelProxy))
                info = ((ViewModelProxy)this).Associated.GetType().GetProperty(propertyName);
            else
                info = this.GetType().GetProperty(propertyName);

            if (info == null)
                return false;
            if (info.PropertyType.IsSubclassOf(typeof(RemotableViewModel)))
                return true;
            // TODO: Check for further checks against INotifyPropertyChanged, which could be mislead by collections!
            if (checkTraditional && typeof(INotifyPropertyChanged).IsAssignableFrom(info.PropertyType) && !typeof(INotifyCollectionChanged).IsAssignableFrom(info.PropertyType))
                return true;

            return false;
        }

        /// <summary>
        /// Adds a remote agent which imports this ViewModel.
        /// </summary>
        /// <param name="agent">The importing agent.</param>
        internal void AddImportingViewModelAgent(IViewModelAgent agent)
        {
            _importingAgents.Add(agent);
        }

        /// <summary>
        /// Removes a remote agent which does no longer import this ViewModel.
        /// </summary>
        /// <param name="agent">The no longer importing agent</param>
        internal void RemoveViewModelAgent(IViewModelAgent agent)
        {
            _importingAgents.Remove(agent);
        }

        /// <summary>
        /// Injects proxy and callback channel into this ViewModel to wire up the connection to the exporting/remote ViewModel.
        /// </summary>
        /// <param name="id">Unique ID of the ViewModel that is imported</param>
        /// <param name="callback">Callback channel</param>
        /// <param name="agent">Proxy</param>
        internal virtual void InjectRemoteViewModelAgent(string id, IViewModelCallback callback, IViewModelAgent agent, IViewModelImporter importer)
        {
            _viewModelLocation = ViewModelLocation.Remote;

            _identifier = id;
            _localCallbackChannel = callback;
            _localCallbackChannel.RemotePropertyChanged += OnRemotePropertyChanged;
            _localCallbackChannel.RemoteCollectionChanged += OnRemoteCollectionChanged;
            _localProxy = agent;

            _importer = importer;

            // An imported ViewModel cannot export and thus not have importing agents.
            _importingAgents.Clear();
        }

        /// <summary>
        /// Removes all previously injected remote objects. (Proxy, CallbackChannel)
        /// </summary>
        internal void RemoveRemoteViewModelAgent()
        {
            _viewModelLocation = ViewModelLocation.Local;

            // Remove all proxy objects
            _localProxy = null;
            if (_localCallbackChannel != null)
            {
                _localCallbackChannel.RemotePropertyChanged -= OnRemotePropertyChanged;
                _localCallbackChannel.RemoteCollectionChanged -= OnRemoteCollectionChanged;
                _localCallbackChannel = null;
            }

            _identifier = string.Empty;

            _importer = null;

            // Since we are just leaving the importing phase, there cannot be any importing agents for an exporting phase yet.
            _importingAgents.Clear();
        }

        /// <summary>
        /// Event handler, is raised whenever the exporting/remote ViewModel sends a
        /// property changed event through the callback channel.
        /// </summary>
        /// <param name="id"></param>
        /// <param name="property"></param>
        internal virtual void OnRemotePropertyChanged(string id, string property)
        {
            // Only fire events that are targeting this actual viewmodel
            if(_identifier != null && _identifier.Equals(id))
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }

        #endregion

        #endregion

        #region INotifyPropertyChanged

        /// <summary>
        /// Event which is raised whenever a setter is called.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Method which is called by each setter whenever a setter is called.
        /// Fires the corresponding event.
        /// </summary>
        /// <param name="property">Name of the property that got changed.</param>
        public void FirePropertyChanged(string property)
        {

            // For remote subscribers
            List<IViewModelAgent> staleImporters = null;
            for (int i = _importingAgents.Count - 1; i >= 0; i--)
            {
                IViewModelAgent importingAgent = _importingAgents[i];
                try
                {
                    importingAgent.FirePropertyChanged(_identifier, property);
                }
                catch (Exception)
                {
                    // Subscriber possible got lost...
                    // RemoveViewModelAgent(_importingAgents[i]);
                    
                    // We want to remove it - but we cannot change the list while we are iterating over it
                    if (staleImporters == null)
                    {
                        staleImporters = new List<IViewModelAgent>();
                    }
                    staleImporters.Add(importingAgent);
                }
            }

            if (staleImporters != null)
            {
                foreach (var importer in staleImporters)
                {
                    RemoveViewModelAgent(importer);
                }
            }

            // For local subscribers
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }

        #endregion
    }
}
